package com.fanvil.videoconference;

import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.hardware.Camera.CameraInfo;
import android.os.Looper;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.TextureView;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
import android.annotation.TargetApi;
//import vdroid.api.FvlApplication;
import com.fanvil.videoconference.FvlLogger;
import com.fanvil.videoconference.FvlSurfaceHolder;

public class FvlVideoProducer {
    private static FvlLogger sLogger = FvlLogger.getLogger(FvlVideoProducer.class.getSimpleName(), FvlLogger.DEBUG);

    public static boolean isRecording = false;

    public static Object startRecording(int cameraId, int width, int height, int fps, int rotation, final long nativePtr) {
        sLogger.d("startRecording(" + cameraId + ", " + width + ", " + height + ", " + fps + ", " + rotation + ", " + nativePtr + ")");
        try {
        Camera camera = Camera.open(cameraId);
        Parameters params = camera.getParameters();

        for (String focusMode : params.getSupportedFocusModes()) {
            if (focusMode.equalsIgnoreCase(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
                sLogger.d("FOCUS_MODE_CONTINUOUS_VIDEO is supported, let's use it");
                params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
                break;
            }
        }

        if (params.isVideoStabilizationSupported()) {
            sLogger.d("Video stabilization is supported, let's use it");
            params.setVideoStabilization(true);
        }

        params.setPreviewSize(width, height);
        int[] chosenFps = findClosestEnclosingFpsRange(fps*1000, params.getSupportedPreviewFpsRange());
        if (chosenFps[0] != chosenFps[1]) {
            // If both values are the same it will most likely break the auto exposure (https://stackoverflow.com/questions/26967490/android-camera-preview-is-dark/28129248#28129248)
            params.setPreviewFpsRange(chosenFps[0], chosenFps[1]);
        }
        camera.setParameters(params);
        setCameraDisplayOrientation(rotation, cameraId, camera);
        camera.startPreview();
        isRecording = true;
        sLogger.d("Returning camera object: " + camera);
        return camera;
        } catch (Exception exc) {
            exc.printStackTrace();
            return null;
        }
    }

    public static void stopRecording(Object cam) {
        isRecording = false;
        sLogger.d("stopRecording(" + cam + ")");
        Camera camera = (Camera) cam;

        if (camera != null) {
            camera.setPreviewCallbackWithBuffer(null);
            camera.stopPreview();
            camera.release();
        } else {
            sLogger.i("Cannot stop recording ('camera' is null)");
        }
    }

    public static void setPreviewDisplaySurface(Object cam, Object surf) {
        sLogger.d("setPreviewDisplaySurface(" + cam + ", " + surf + ")");
        Camera camera = (Camera) cam;
        try {
            if (surf instanceof  SurfaceView) {
                SurfaceView surface = (SurfaceView) surf;
                camera.setPreviewDisplay(surface.getHolder());
            } else if (surf instanceof TextureView && ((TextureView) surf).isAvailable()) {
                camera.setPreviewTexture(((TextureView) surf).getSurfaceTexture());
            } else if (surf instanceof SurfaceTexture) {
                sLogger.d("setPreviewDisplaySurface(SurfaceTexture)");
                camera.setPreviewTexture((SurfaceTexture) surf);
            } else {
                sLogger.w("setPreviewDisplaySurface: unknown preview display surface");
            }
        } catch (Exception exc) {
            exc.printStackTrace();
        }
    }

    static public void activateAutoFocus(Object cam) {
        sLogger.d("Turning on autofocus on camera " + cam);
        Camera camera = (Camera) cam;
        if (camera != null && (camera.getParameters().getFocusMode() == Parameters.FOCUS_MODE_AUTO || camera.getParameters().getFocusMode() == Parameters.FOCUS_MODE_MACRO))
            camera.autoFocus(null); // We don't need to do anything after the focus finished, so we don't need a callback
    }

    /**
     * Default: no front; rear=0; default=rear
     *
     */
    static public  class AndroidCamera {
        static public class Size{
            public final int width;
            public final int height;

            public Size(int w, int h){
                this.width = w;
                this.height = h;
            }
        }

        public AndroidCamera(int i, boolean f, List<Size> r, int o) {
            this.id = i;
            this.frontFacing = f;
            this.orientation = o;
            this.resolutions = r;
        }
        public AndroidCamera(int i, boolean f, int o, List<android.hardware.Camera.Size> origin) {
            this.resolutions = new ArrayList<Size>(origin.size());
            for (android.hardware.Camera.Size s : origin) {
                this.resolutions.add(new AndroidCamera.Size(s.width,s.height));
            }
            this.id = i;
            this.frontFacing = f;
            this.orientation = o;
        }
        public int id;
        public boolean frontFacing; // false => rear facing
        public int orientation;
        public List<Size> resolutions;

    }

    static public AndroidCamera[] probeCameras() {
        List<AndroidCamera> cam = new ArrayList<AndroidCamera>(Camera.getNumberOfCameras());

        for(int i=0; i<Camera.getNumberOfCameras(); i++) {
            CameraInfo info = new CameraInfo();
            Camera.getCameraInfo(i, info);
            Camera c = Camera.open(i);
            cam.add(new AndroidCamera(i, info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT, info.orientation, c.getParameters().getSupportedPreviewSizes()));
            c.release();
        }

        AndroidCamera[] result = new AndroidCamera[cam.size()];
        result = cam.toArray(result);
        return result;
    }

    //select nearest resolution equal or above requested, if none, return highest resolution from the supported list
    private static int[] selectNearestResolutionAvailable(int id, int requestedW, int requestedH) {
        // inversing resolution since webcams only support landscape ones
        if (requestedH > requestedW) {
            int t = requestedH;
            requestedH = requestedW;
            requestedW = t;
        }

        AndroidCamera[] cameras = probeCameras();
        List<AndroidCamera.Size> supportedSizes = null;
        for(AndroidCamera c: cameras) {
            if (c.id == id)
                supportedSizes = c.resolutions;
        }
        if (supportedSizes == null) {
        sLogger.e("Failed to retrieve supported resolutions.");
            return null;
        }
        sLogger.i(supportedSizes.size() + " supported resolutions :");
        for(AndroidCamera.Size s : supportedSizes) {
            sLogger.i(s.width + "x" + s.height);
        }
        int r[] = null;

        int rW = Math.max(requestedW, requestedH);
        int rH = Math.min(requestedW, requestedH);

        try {
            // look for nearest size
            AndroidCamera.Size result = supportedSizes.get(0); /*by default return first value*/
            int req = rW * rH;
            int minDist = Integer.MAX_VALUE;
            int useDownscale = 0;
            for(AndroidCamera.Size s: supportedSizes) {
                int dist = /*Math.abs*/-1*(req - s.width * s.height);
                if ( ((s.width >= rW && s.height >= rH) || (s.width >= rH && s.height >= rW)) && dist < minDist) {
                    minDist = dist;
                    result = s;
                    useDownscale = 0;
                }

                /* MS2 has a NEON downscaler, so we test this too */
                int downScaleDist = /*Math.abs*/-1*(req - s.width * s.height / 4);
                if (((s.width/2 >= rW && s.height/2 >= rH) || (s.width/2 >= rH && s.height/2 >= rW)) && downScaleDist < minDist) {
                        minDist = downScaleDist;
                        result = s;
                        useDownscale = 1;
                }
                if (s.width == rW && s.height == rH) {
                    result = s;
                    useDownscale = 0;
                    break;
                }
            }
            r = new int[] {result.width, result.height, useDownscale};
            sLogger.i("resolution selection done (" + r[0] + ", " + r[1] + ", " + r[2] + ")");
            return r;
        } catch (Exception exc) {
            sLogger.e(" resolution selection failed");
            return null;
        }
    }
    private static void setCameraDisplayOrientation(int rotationDegrees, int cameraId, Camera camera) {
        android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(cameraId, info);

        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + rotationDegrees) % 360;
            result = (360 - result) % 360; // compensate the mirror
        } else { // back-facing
            result = (info.orientation - rotationDegrees + 360) % 360;
        }

        sLogger.w("Camera preview orientation: "+ result);
        try {
            camera.setDisplayOrientation(result);
        } catch (Exception exc) {
            sLogger.e("Failed to execute: camera[" + camera + "].setDisplayOrientation(" + result + ")");
            exc.printStackTrace();
        }
    }

    private static int[] findClosestEnclosingFpsRange(int expectedFps, List<int[]> fpsRanges) {
        sLogger.d("Searching for closest fps range from " + expectedFps);
        if (fpsRanges == null || fpsRanges.size() == 0) {
            return new int[] { 0, 0 };
        }

        // init with first element
        int[] closestRange = fpsRanges.get(0);
        int measure = Math.abs(closestRange[0] - expectedFps) + Math.abs(closestRange[1] - expectedFps);
        for (int[] curRange : fpsRanges) {
            if (curRange[0] > expectedFps || curRange[1] < expectedFps) continue;
            int curMeasure = Math.abs(curRange[0] - expectedFps) + Math.abs(curRange[1] - expectedFps);
            if (curMeasure < measure && curRange[0] != curRange[1]) { // If both values are the same it will most likely break the auto exposure (https://stackoverflow.com/questions/26967490/android-camera-preview-is-dark/28129248#28129248)
                closestRange = curRange;
                measure = curMeasure;
                sLogger.d("A better range has been found: w=" + closestRange[0] + ",h=" + closestRange[1]);
            }
        }
        sLogger.d("The closest fps range is w=" + closestRange[0] + ",h=" + closestRange[1]);
        return closestRange;
    }
}
